/**
 * This file is part of Sonedyan.
 * 
 * Sonedyan is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public
 * License as published by the Free Software Foundation;
 * either version 3 of the License, or (at your option) any
 * later version.
 *
 * Sonedyan is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied
 * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
 * PURPOSE.  See the GNU General Public License for more
 * details.
 *
 * You should have received a copy of the GNU General Public
 * License along with Octave; see the file COPYING.  If not
 * see <http://www.gnu.org/licenses/>.
 * 
 * Copyright (C) 2009-2012 Jimmy Dubuisson <jimmy.dubuisson@gmail.com>
 */

package org.unige.mpej.eckmann.sonedyan.fns.db;

import java.io.BufferedReader;
import java.io.FileReader;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Collections;
import java.util.Date;
import java.util.Hashtable;
import java.util.Set;
import java.util.TreeMap;
import java.util.Vector;

import org.apache.log4j.Logger;
import org.unige.mpej.eckmann.sonedyan.fns.bean.Actor;
import org.unige.mpej.eckmann.sonedyan.fns.bean.Link;
import org.unige.mpej.eckmann.sonedyan.fns.bean.Actor.Status;
import org.unige.mpej.eckmann.sonedyan.utils.DataUtils;

import edu.uci.ics.jung.graph.SparseMultigraph;
import edu.uci.ics.jung.graph.util.EdgeType;

/**
 * class used to load the FNS dataset
 */
public class DatasetLoader 
{
	private static Logger log = Logger.getLogger(org.unige.mpej.eckmann.sonedyan.fns.db.DatasetLoader.class);
	
	private static final String DS_FILENAME = "data-fns.txt";
	private static final String ARFF_FILENAME = "data.arff";
	
	private String filePath;
	
	// inner class for storing temporary data 
	private class ActorStatus
	{
		private Actor actor;
		private Status status;
		
		private String projectId;
		private Date startDate;
		private Date endDate;
		
		public Actor getActor()
		{
			return actor;
		}

		public void setActor(Actor actor)
		{
			this.actor = actor;
		}

		public Status getStatus()
		{
			return status;
		}

		public void setStatus(Status status)
		{
			this.status = status;
		}

		public String getProjectId()
		{
			return projectId;
		}

		public void setProjectId(String projectId)
		{
			this.projectId = projectId;
		}

		public Date getStartDate()
		{
			return startDate;
		}

		public void setStartDate(Date startDate)
		{
			this.startDate = startDate;
		}

		public Date getEndDate()
		{
			return endDate;
		}

		public void setEndDate(Date endDate)
		{
			this.endDate = endDate;
		}
		
	}
	
	/**
	 * default class constructor
	 */
	public DatasetLoader()
	{
		this.filePath = DS_FILENAME;
	}
	
	/**
	 * class constructor
	 */
	public DatasetLoader(String filePath)
	{
		this.filePath = filePath;
	}
	
	/**
	 * load dataset
	 */
	public SparseMultigraph<Actor, Link> loadDataset()
	{
		return loadDataset(this.filePath);
	}
	
	/**
	 * load dataset
	 */
	public SparseMultigraph<Actor, Link> loadDataset(String fileName)
	{
		BufferedReader br = null;
		SparseMultigraph<Actor, Link> multiGraph = new SparseMultigraph<Actor, Link>();
		Hashtable<String, Actor> actors = new Hashtable<String, Actor>();
		
		SimpleDateFormat completeFormatter = new SimpleDateFormat(DataUtils.INPUT_DATE_FORMAT);
		SimpleDateFormat reducedFormatter = new SimpleDateFormat(DataUtils.REDUCED_DATE_FORMAT);	
		
		// keep track of project ids and start dates for reporting purposes
		Vector<String> projectIds = new Vector<String>();
		Hashtable<Date, Integer> projectDates = new Hashtable<Date, Integer>();
		
		try
		{
			// keep track of min and max date
			Date minDate = reducedFormatter.parse("01.2100");
			Date maxDate = reducedFormatter.parse("01.1900");
			
			br = new BufferedReader(new FileReader(fileName));
			
			String line;
				
			while ((line = br.readLine()) != null)
			{
				if (!line.startsWith("#"))
				{
					String[] columns = line.split(" ");
					
					String projectId = columns[0];
					String dateString = columns[2];
					int duration = new Integer(columns[3]);
					
					Date startDate = completeFormatter.parse(dateString);
					
					// log.debug("Project ID: '" + projectId + "', Duration: '" + duration + "', Start date: '" + startDate + "'");
					if (projectIds.contains(projectId))
					{
						log.error("Project ID '" + projectId + "' was already added!");
					}
					
					// add the project ID to the list
					projectIds.add(projectId);
					
					// add 1 to the starting date multiplicity
					Integer dateMultiplicity = projectDates.get(startDate);
					
					if (dateMultiplicity == null)
					{
						projectDates.put(startDate, 1);
					}
					else
					{
						dateMultiplicity += 1;
						projectDates.put(startDate, dateMultiplicity);
					}
					
					Calendar calendar = Calendar.getInstance();
					calendar.setTime(startDate);
					calendar.add(Calendar.MONTH, duration);
					Date endDate = calendar.getTime();
					
					// update min and max dates
					if (startDate.before(minDate)){ minDate = startDate; }
					if (endDate.after(maxDate)){ maxDate = endDate; }
					
					// get the set of status of a given actor in a given project
					Hashtable<Actor, Vector<Status>> actorsStatus = new Hashtable<Actor, Vector<Status>>();
					
					for (int i = 4; i < columns.length; i++)
					{
						// get the set of ids
						String id = columns[i];
						
						if (id != null && id.length() > 0)
						{
							// log.debug("ID: '" + id + "'");
							
							Actor actor = null;
							Status status = null;
							
							// postdoc actor
							if (id.startsWith("+"))
							{
								id = id.substring(1);
								actor = actors.get(id);
								
								// log.debug("ID (Postdoc): '" + id + "'"); 
								
								// add new vertex to the graph
								if (actor == null)
								{
									actor = new Actor();
									actor.setId(id);
									
									actors.put(id, actor);
									
									// log.debug("Adding actor: " + id);
									multiGraph.addVertex(actor);
								}
								
								status = Actor.Status.POSTDOC;
							}
							else if (id.startsWith("-"))
							{
								id = id.substring(1);
								actor = actors.get(id);
								
								// log.debug("ID (Phd): '" + id + "'");
								
								// add new vertex to the graph
								if (actor == null)
								{
									actor = new Actor();
									actor.setId(id);
									
									actors.put(id, actor);
									
									// log.debug("Adding actor: " + id);
									multiGraph.addVertex(actor);
								}
								
								status = Actor.Status.PHD;
							}
							else
							{
								actor = actors.get(id);
								
								// add new vertex to the graph
								if (actor == null)
								{
									actor = new Actor();
									actor.setId(id);
									
									actors.put(id, actor);
									
									// log.debug("Adding actor: " + id);
									multiGraph.addVertex(actor);
								}
								
								status = Actor.Status.PROFESSOR;
							}
							
							Vector<Status> storedStatus = actorsStatus.get(actor);
							
							if (storedStatus == null)
							{
								storedStatus = new Vector<Status>();
							}
							
							if (!storedStatus.contains(status))
							{
								storedStatus.add(status);
							}
							
							actorsStatus.put(actor, storedStatus);
						} // if (id != null)
					} // for
					
					// set of actors involved in a given project
					Set<Actor> actorsSet = actorsStatus.keySet();
					
					Vector<ActorStatus> rows = new Vector<ActorStatus>();
					
					// if there is more than 1 person involved in the project
					if (actorsSet.size() > 1)
					{
						for (Actor actor: actorsSet)
						{
							// get the different status of the specified actor
							Vector<Status> status = actorsStatus.get(actor);
							
							// we might assume that a given person cannot have more than 2 different status in a given project
							// we also assume that the status are ordered in the following way: PHD -> POSTDOC -> PROFESSOR
							if (status.size() > 1)
							{
								// log.debug("Actor '" + actor.getId() + "' has 2 different status");
								
								// sort the status
								Collections.sort(status);
								
								int newDuration = (int) Math.floor(duration / status.size());
								Date newStartDate = startDate;
								
								calendar.setTime(newStartDate);
								calendar.add(Calendar.MONTH, newDuration);
								Date newEndDate = calendar.getTime();
								
								int counter = 1;
								
								for (Status s: status)
								{
									ActorStatus row = new ActorStatus();
									
									row.setActor(actor);
									row.setStatus(s);
									row.setProjectId(projectId);
									row.setStartDate(newStartDate);
									row.setEndDate(newEndDate);
									
									// log.debug("Adding row for actor " + actor.getId() + "/" + s + " (project ID: '" + projectId + "', start date: " + newStartDate + ", end date: " + newEndDate + ")");
									
									// add the remaining
									if (counter == (status.size() - 1))
									{
										newDuration = newDuration + (duration % status.size());
									}
									
									// set the new start date to current end date
									// and add the specified number of month to the new end date
									newStartDate = newEndDate;
									calendar.setTime(newStartDate);
									calendar.add(Calendar.MONTH, newDuration);
									newEndDate = calendar.getTime();
									
									counter++;
									
									rows.add(row);
								}
							}
							else
							{
								ActorStatus row = new ActorStatus();
								
								row.setActor(actor);
								row.setStatus(status.get(0));
								row.setProjectId(projectId);
								row.setStartDate(startDate);
								row.setEndDate(endDate);
								
								rows.add(row);
							}
						} // for
						
						Vector<String> done = new Vector<String>();
						
						// generate the complete set of links
						for (ActorStatus row1 : rows)
						{
							for (ActorStatus row2 : rows)
							{
								Actor actor1 = row1.getActor();
								Actor actor2 = row2.getActor();
								
								// node with 2 different status will appear first (the implied time validity for the link is lower than the whole project duration)
								// check that:
								// 1. the 2 actors are not the same
								// 2. a link was not established between them before
								// 3. fromActor validity date is included in toActor validity date 
								if (actor1 != actor2 
										&& (!done.contains(actor1.getId() + "/" + row1.getStatus() + "->" + actor2.getId() + "/" + row2.getStatus()) && !done.contains(actor2.getId() + "/" + row2.getStatus() + "->" + actor1.getId() + "/" + row1.getStatus())) 
										&& (row1.getStartDate().getTime() >= row2.getStartDate().getTime() && row1.getEndDate().getTime() <= row2.getEndDate().getTime()) )
								{
									done.add(actor1.getId() + "/" + row1.getStatus() + "->" + actor2.getId() + "/" + row2.getStatus());
									done.add(actor2.getId() + "/" + row2.getStatus() + "->" + actor1.getId() + "/" + row1.getStatus());
									
									Link link = new Link();
									link.setLinkId(projectId);
									
									link.setStartDate(row1.getStartDate());
									link.setEndDate(row1.getEndDate());
									
									link.setFromActorId(actor1.getId());
									link.setToActorId(actor2.getId());
									
									link.setFromStatus(row1.getStatus());
									link.setToStatus(row2.getStatus());
									
									// log.debug("Adding link (" + actor1.getId() + "/" + row1.getStatus() + " -> " + actor2.getId() + "/" + row2.getStatus() + " (Project ID: '" + projectId + "')");
									
									multiGraph.addEdge(link, actor1, actor2, EdgeType.UNDIRECTED);
								}
							}
						}
					} // if there is more than 1 person involved in the project	
				} // if (not a comment line)
			} // while
			
			log.info("Min date: " + reducedFormatter.format(minDate));
			log.info("Max date: " + reducedFormatter.format(maxDate));
			
			log.info("Number of projects: " + projectIds.size());
			
			TreeMap<Date, Double> projectsDataset = new TreeMap<Date, Double>(); 
			Date tDate = minDate;
			Calendar cal = Calendar.getInstance();
			cal.setTime(minDate);
			
			while (tDate.compareTo(maxDate) <= 0)
			{
				Integer multiplicity = projectDates.get(tDate);
				
				if (multiplicity != null)
					projectsDataset.put(tDate, (double) multiplicity);
				else
					projectsDataset.put(tDate, (double) 0);
				
				cal.add(Calendar.MONTH, 1);
				tDate = cal.getTime();
			}
			
			Vector<TreeMap<Date, Double>> projectsDatasets = new Vector<TreeMap<Date, Double>>();
			projectsDatasets.add(projectsDataset);
			DataUtils.writeTimeSeriesDatasets("projects.dat", projectsDatasets);
			
			return multiGraph;
		}
		catch(Exception e)
		{
			log.error("An error occured: " + e.getMessage());
			
			return null;
		}
		finally
		{
			try
			{
				if (br != null)
				{ 
					br.close(); 
				}
			}
			catch(Exception e){}
		}	
	}
	
	/**
	 * import ARFF
	 */
	public static SparseMultigraph<Actor, Link> loadArffDataset()
	{
		return loadArffDataset(ARFF_FILENAME);
	}
	
	/**
	 * load ARFF data
	 */
	public static SparseMultigraph<Actor, Link> loadArffDataset(String filePath)
	{
		BufferedReader br = null;
		SparseMultigraph<Actor, Link> multiGraph = new SparseMultigraph<Actor, Link>();
		Hashtable<String, Actor> actors = new Hashtable<String, Actor>();
		
		SimpleDateFormat formatter = new SimpleDateFormat(DataUtils.REDUCED_DATE_FORMAT);
		
		try
		{
			br = new BufferedReader(new FileReader(filePath));
			
			String line;
			
			int vCounter = 0;
			int eCounter = 0;
			
			while ((line = br.readLine()) != null)
			{
				if (line.length() > 0 && !line.startsWith("%") && !line.startsWith("@"))
				{
					String[] columns = line.split(",");
					
					String fromId = columns[0];
					String fromStatusString = columns[1];
					String toId = columns[2];
					String toStatusString = columns[3];
					String projectId = columns[4];
					Date startDate = formatter.parse(columns[5]);
					Date endDate = formatter.parse(columns[6]);
					
					Status fromStatus;
					
					if (fromStatusString.equals("PHD")) { fromStatus = Actor.Status.PHD; }
					else if (fromStatusString.equals("POSTDOC")) { fromStatus = Actor.Status.POSTDOC; }
					else { fromStatus = Actor.Status.PROFESSOR; }
					
					Status toStatus;
					
					if (toStatusString.equals("PHD")) { toStatus = Actor.Status.PHD; }
					else if (toStatusString.equals("POSTDOC")) { toStatus = Actor.Status.POSTDOC; }
					else { toStatus = Actor.Status.PROFESSOR; }
					
					Actor fromActor;
					
					// add the actors if necessary
					if ((fromActor = actors.get(fromId)) == null)
					{
						fromActor = new Actor();
						fromActor.setId(fromId);
						actors.put(fromId, fromActor);
						multiGraph.addVertex(fromActor);
						vCounter++;
					}
					
					Actor toActor;
					
					if ((toActor = actors.get(toId)) == null)
					{
						toActor = new Actor();
						toActor.setId(toId);
						actors.put(toId, toActor);
						multiGraph.addVertex(toActor);
						vCounter++;
					}
					
					// add the edge
					Link link = new Link();
					link.setFromActorId(fromId);
					link.setFromStatus(fromStatus);
					link.setToActorId(toId);
					link.setToStatus(toStatus);
					link.setLinkId(projectId);
					link.setStartDate(startDate);
					link.setEndDate(endDate);
					
					multiGraph.addEdge(link, fromActor, toActor, EdgeType.UNDIRECTED);
					eCounter++;
				}
			} // while
			
			log.info("Arff file loaded (# vertices: '" + vCounter + "', # edges: '" + eCounter + "')");
			
			return multiGraph;
		}
		catch(Exception e)
		{
			log.error("An error occured: " + e.getMessage());
			
			return null;
		}
		finally
		{
			try
			{
				if (br != null)
				{ 
					br.close(); 
				}
			}
			catch(Exception e){}
		}
	}
	
}
